Проанализируйте ассортимент товаров на основе транзакций интернет-магазина товаров для дома и быта:
import pandas as pd
import datetime as dt
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import plotly.express as px
import scipy.stats as stats
# загружаем датасет из файла
df = pd.read_csv('', sep=',')
# выводим пять первых строк датасета
df.head()
| date | customer_id | order_id | product | quantity | price | |
|---|---|---|---|---|---|---|
| 0 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | Комнатное растение в горшке Алое Вера, d12, h30 | 1 | 142.0 |
| 1 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | Комнатное растение в горшке Кофе Арабика, d12,... | 1 | 194.0 |
| 2 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | Радермахера d-12 см h-20 см | 1 | 112.0 |
| 3 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | Хризолидокарпус Лутесценс d-9 см | 1 | 179.0 |
| 4 | 2018100100 | ee47d746-6d2f-4d3c-9622-c31412542920 | 68477 | Циперус Зумула d-12 см h-25 см | 1 | 112.0 |
Видим, что:
# посмотрим форматы даных
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 6737 entries, 0 to 6736 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date 6737 non-null int64 1 customer_id 6737 non-null object 2 order_id 6737 non-null int64 3 product 6737 non-null object 4 quantity 6737 non-null int64 5 price 6737 non-null float64 dtypes: float64(1), int64(3), object(2) memory usage: 315.9+ KB
Видим, что:
# преобразуем имена и названия к типу string, чтобы мы могли изменять значения и анализировать их
df['customer_id'] = df['customer_id'].astype('string')
df['product'] = df['product'].astype('string')
# преобразуем содержание временных столбцов к типу datetime
df['date'] = df['date'].astype('string')
df['date'] = pd.to_datetime(df['date'], format='%Y%m%d%H')
# проверим, поменялись ли форматы данных
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 6737 entries, 0 to 6736 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date 6737 non-null datetime64[ns] 1 customer_id 6737 non-null string 2 order_id 6737 non-null int64 3 product 6737 non-null string 4 quantity 6737 non-null int64 5 price 6737 non-null float64 dtypes: datetime64[ns](1), float64(1), int64(2), string(2) memory usage: 315.9 KB
# посмотрим распределение событий по датам
df['date'].hist(bins=150);
# называем оси и заголовок
plt.xlabel('Время')
plt.ylabel('Количество')
plt.title('Распределение событий по датам')
plt.show()
# находим минимальную и максимальную даты в датасете
display(df['date'].min())
display(df['date'].max())
print("Данные охватывают", df['date'].max() - df['date'].min())
Timestamp('2018-10-01 00:00:00')
Timestamp('2019-10-31 16:00:00')
Данные охватывают 395 days 16:00:00
Видим, что:
# посмотрим распределение количества товаров
df['quantity'].hist(bins=100);
# называем оси и заголовок
plt.xlabel('Количество товаров')
plt.ylabel('Частота')
plt.title('Распределение количества товаров')
plt.show()
# проверим адекватность айди заказов
display(df['order_id'].astype('int').hist())
<AxesSubplot:>
Видим, что:
В основном покупатели берут штучный товар, т.е. это розничный магазин - из дальнейшего анализа необходимо будет исключить оптовые закупки, если таковые будут.
Номера заказов расположены не последовательно, поэтому стоит задать вопрос специалистам техподдержки магазина, по какой причине нумерация прерывается.
# посмотрим распределение цены товаров
df['price'].hist(bins=100);
# называем оси и заголовок
plt.xlabel('Цена')
plt.ylabel('Частота')
plt.title('Распределение цены товаров')
plt.show()
Видим, что:
Цена покупаемого товара в основном не превышает 1000 у.е.
# добавим столбцы с указанием года, месяца и недели в датасет
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['week'] = df['date'].dt.isocalendar().week
# добавим столбцы с указанием дня недели и часа
df['dow'] = df['date'].dt.dayofweek + 1
df['hour'] = df['date'].dt.hour
# также добавим столбец с сочетанием года и месяца
df['ymonth'] = df['year'].astype('string') + df['month'].astype('string')
df['ymonth'] = pd.to_datetime(df['ymonth'], format='%Y%m')
# добавим столбец с суммой выручки за товар как произведения цены и количества
df['revenue'] = df['price'] * df['quantity']
# попробуем добавить столбец с коротким ID покупателей, ограничив его шестью последними знаками
# сколько уникальных покупателей в датасете?
display(df['customer_id'].nunique())
2451
# добавляем столбец
df['cid'] = [x.strip()[-7:] for x in df['customer_id']]
# проверяем, не склеились ли ID
display(df['cid'].nunique())
2451
# добавим столбец с двумя первыми словами из наименования товаров
df['prod'] = [x.split()[0] + ' ' + x.split()[1] for x in df['product']]
# выводим количество пропусков в каждом столбце датасета
df.isna().sum()
date 0 customer_id 0 order_id 0 product 0 quantity 0 price 0 year 0 month 0 week 0 dow 0 hour 0 ymonth 0 revenue 0 cid 0 prod 0 dtype: int64
# выводим количество полных дубликатов в датасете
df.duplicated().sum()
0
Вывод:
Полных дубликатов не обнаружено
# посмотрим, сколько в нашем датасете дубликатов по основным колонкам, за исключением колонки со временем заказа
# наличие таких дубликатов свидетельствует о сбоях в системе заказов либо об автоматических заказах
display(df.drop_duplicates(subset=['customer_id', 'order_id', 'product', 'quantity', 'price'])['date'].count() / df['date'].count())
0.7233189847112959
Такие заказы, составляющие почти 28% датасета, нужно исключить из анализа
# удаляем их
df = df.drop_duplicates(subset=['customer_id', 'order_id', 'product', 'quantity', 'price'])
# далее проверим адекватность цен на товары стоимостью более 2000 у.е.
# не было ли ошибок при вводе этих цен
display(df.query('price >= 2000')
.pivot_table(index='prod', values='price', aggfunc='max'))
| price | |
|---|---|
| prod | |
| Tepмокружка AVEX | 2399.0 |
| Автоматическая щетка | 7229.0 |
| Афеляндра скуарроса | 3524.0 |
| Бак для | 3749.0 |
| Ведро для | 3749.0 |
| Весы напольные | 2849.0 |
| Гладильная доска | 7424.0 |
| Гладильная доска-стремянка | 2399.0 |
| Гортензия Микс | 3599.0 |
| Двуспальное постельное | 2024.0 |
| Доска гладильная | 3299.0 |
| Ерш для | 3524.0 |
| Карниз алюминиевый | 2099.0 |
| Коврик для | 5474.0 |
| Коврик придверный | 2009.0 |
| Комплект для | 5399.0 |
| Котел алюминиевый | 2924.0 |
| Мантоварка-пароварка WEBBER | 2219.0 |
| Мусорный контейнер | 5512.0 |
| Набор Vileda | 2924.0 |
| Набор инструментов | 5399.0 |
| Наматрасник Wellness | 3074.0 |
| Новогоднее дерево | 3524.0 |
| Одеяло Wellness | 4724.0 |
| Покрывало жаккард | 6134.0 |
| Полки QWERTY | 4312.0 |
| Пылесос DELTA | 2249.0 |
| Сиденье для | 6149.0 |
| Скатерть 350х150 | 2249.0 |
| Скатерть Арлет | 2174.0 |
| Скатерть Джулия | 2249.0 |
| Стремянка 5 | 3974.0 |
| Стремянка 7 | 7724.0 |
| Стремянка COLOMBO | 3449.0 |
| Стремянка Colombo | 2699.0 |
| Стремянка FRAMAR | 4499.0 |
| Стремянка Scab | 5549.0 |
| Стремянка алюминиевая | 4949.0 |
| Стремянка-табурет алюминиевая | 2699.0 |
| Стремянки Colombo | 3974.0 |
| Сумка-тележка 2-х | 2849.0 |
| Сумка-тележка 3-х | 2699.0 |
| Сумка-тележка TWIN | 2624.0 |
| Сумка-тележка хозяйственная | 8737.0 |
| Сушилка Meliconi | 5594.0 |
| Сушилка для | 7004.0 |
| Сушилка уличная | 14917.0 |
| Урна уличная | 7349.0 |
| Урна-пепельница из | 5287.0 |
| Фал капроновый | 2099.0 |
| Цитрофортунелла Кумкват | 3074.0 |
| Швабра для | 2624.0 |
| Швабра хозяйственная | 3224.0 |
| Штора для | 4424.0 |
Мы проверили цены указанных выше товаров в интернете и нашли, что они значительно не отличаются от представленных в датасете
# далее проверим, нет ли заказов, приписываемых сразу нескольким покупателям
bugged_orders = (
df.pivot_table(index='order_id', values='customer_id', aggfunc='nunique') # таблица "заказ" - "уник кол-во покупателей"
.reset_index()
.sort_values('customer_id', ascending=False)
.query('customer_id > 1')['order_id'] # если заказ сделан более чем одним покупателем, то это ошибка
.to_list()
)
display(bugged_orders)
[72845, 71480, 69485, 69310, 69833, 72790, 72778, 14872, 71542, 71054, 71663, 70726, 69531, 70542, 70903, 69283, 71226, 71571, 69410, 69345, 70808, 70114, 70631, 71461, 72950, 71648, 70946, 68785, 72188]
# какой процент данных занимают эти записи?
display(df.query('order_id in @bugged_orders')['order_id'].count() / df['order_id'].count())
0.013544018058690745
1,3%. Удаляем
# удаляем
df = df.query('order_id not in @bugged_orders')
Вывод:
Мы провели предобработку данных, очистив датасет от возможных ошибок и дубликатов
Привели данные к удобным типам и добавили столбцы, которые помогут нам в дальнейшем анализе
Сперва посмотрим распределение кол-ва покупаемого товара, чтобы исключить из анализа оптовые закупки
# построим график размаха продаж
sns.boxplot(x=df['quantity'], orient='h')
# называем график и его оси
plt.xlabel('Количество товара, шт')
plt.title('Диаграмма размаха количества товара')
plt.show()
# построим дополнительный детальный график, ограничив диапазон кол-ва предыдущего до 200 шт
sns.boxplot(x=df['quantity'], orient='h')
# ограничиваем ось Х
plt.axis([0, 200, None, None])
# называем график и его оси
plt.xlabel('Количество товара, шт')
plt.title('Детальный размах количества товара')
plt.show()
# посмотрим, какие товары покупают оптом
display(df.query('quantity > 50')
.pivot_table(index='prod', values='quantity', aggfunc='sum')
.reset_index()
.sort_values('quantity', ascending=False)
.head(15)
.plot(x='prod', y='quantity', kind='bar')); # строим диаграмму
# называем оси и заголовок
plt.xlabel('Товар')
plt.ylabel('Количество в шт')
plt.title('Оптом у магазина покупают следующие товары')
plt.show()
<AxesSubplot:xlabel='prod'>
Видим, что:
В датасете есть много оптовых закупок. Принимая во внимание, что ассортимент магазина включает немало мелких и/или парных предметов (вилки, рассада, крючки и пр.), будем считать оптовыми такие операции, в которых закупается более чем 50 экземпляров товара
# смотрим, какую долю составляют операции к удалению
display(len(df.query('quantity > 50')) / len(df))
# сколько выручки приносят оптовые закупки?
display(df.query('quantity > 50')['revenue'].sum())
0.004784688995215311
880158.0
# также проверим, что наибольшую выручку приносят именно операции продажи товара в одном экземпляре
# (в одном заказе может быть несколько операций)
revenue_top_quantity = (df.pivot_table(index='quantity', values='revenue', aggfunc='sum')
.reset_index()
.sort_values('revenue', ascending=False)
) # cводная таблица с нужными цифрами, отсортированная по убыванию кол-ва точек
revenue_top_quantity.head()
| quantity | revenue | |
|---|---|---|
| 0 | 1 | 2258065.0 |
| 48 | 1000 | 675000.0 |
| 1 | 2 | 228022.0 |
| 9 | 10 | 107450.0 |
| 2 | 3 | 86046.0 |
# Посмотрим на динамику оптовых продаж, прежде чем исключить их
revenue_by_month = (df.query('quantity > 50')
.pivot_table(index='ymonth', values='revenue', aggfunc='sum')
.reset_index()
.sort_values('ymonth', ascending=True)
) # cводная таблица с нужными цифрами, отсортированная по убыванию кол-ва точек
revenue_by_month.plot(x='ymonth', y='revenue', kind='bar') # строим диаграмму
# называем оси и заголовок
plt.xlabel('Месяц')
plt.ylabel('Выручка в у.е.')
plt.title('Распределение оптовой выручки по месяцам')
plt.show()
Итак, оптовые покупки составляют 0,5% от объёма данных. Из таблицы и помесячного графика выручки мы видим, что 675 тыс у.е. оптовой выручки приходится на одну аномальную операцию, совершённую в июне 2019 года. Исключив её, а также операции покупки более 50 экземпляров товара, мы получим адекватные данные о розничных закупках
# исключаем оптовые закупки, которые составляют 0,5% операций,
# в том числе разовый заказ на 675 тыс у.е.
df = df.query('quantity <= 50')
# посмотрим распределение цен проданных товаров - построим график размаха
sns.boxplot(x=df['price'], orient='h')
# называем график и его оси
plt.xlabel('Цена, у.е.')
plt.title('Диаграмма размаха цен')
plt.show()
# построим дополнительный детальный график, ограничив диапазон цены предыдущего до 2 тыс у.е.
sns.boxplot(x=df['price'], orient='h')
# ограничиваем ось Х
plt.axis([0, 2000, None, None])
# называем график и его оси
plt.xlabel('Цена, у.е.')
plt.title('Детальный размах цен')
plt.show()
Видим, что:
Основная часть товаров продаётся по цене от 50 до 500 у.е. за штуку. Аномально высокая цена товара начинается уже на уровне 1 000 у.е.
Однако ранее мы уже проанализировали цены выше 2 000 у.е. и заключили, что ошибок ввода или подозрительно высоких цен в них нет.
Поэтому мы не исключаем из анализа дополнительные данные
На этом этапе мы исключили из анализа оптовые закупки
# посмотрим, сколько покупателей и заказов есть в логах
display(df['customer_id'].nunique())
display(df['order_id'].nunique())
# сколько заказов приходится в среднем на покупателя?
display(df['order_id'].nunique() / df['customer_id'].nunique())
2375
2734
1.1511578947368422
# посмотрим, сколько выручки получила компания за период
display(df['revenue'].sum())
# сколько выручки приходится в среднем на покупателя?
display(df['revenue'].sum() / df['customer_id'].nunique())
3214457.0
1353.4555789473684
# посмотрим, сколько товаров составляют ассортимент магазина
display(df['product'].nunique())
# сколько товаров приходится в среднем на заказ?
display(df['product'].count() / df['order_id'].nunique())
2318
1.7498171177761521
Выводы:
# добавим файл с проставленными нами парами "товар"-"категория" отдельным датасетом
df_cat = pd.read_csv('https://downloader.disk.yandex.ru/disk/3040a2d8dc83a71b1a4e37c11a64a0d6d2f07580785bb39a6e564b8f0751c2f6/64960832/Kxhhxzo7VermcTtygkQTfWBQydlZ4zbI96VGP8F9-nCStzqYYa-ZdbhSmF2fovL2NA9CJ-2A5sinEp9530_-oA%3D%3D?uid=0&filename=cat1.csv&disposition=attachment&hash=XzzcjF4wOn/8Y0L%2BtDzQtJx9gItFqHPqpUukXHSsXQRfJL6sf6H754dubHvwfObqq/J6bpmRyOJonT3VoXnDag%3D%3D&limit=0&content_type=text%2Fplain&owner_uid=34392834&fsize=277393&hid=430a5ebe6a78b848daf71b414cc1a8fd&media_type=spreadsheet&tknv=v2', sep=',')
# выведем его на экран
df_cat.head()
| product | cat | |
|---|---|---|
| 0 | Комнатное растение в горшке Алое Вера, d12, h30 | растения |
| 1 | Комнатное растение в горшке Кофе Арабика, d12,... | растения |
| 2 | Радермахера d-12 см h-20 см | растения |
| 3 | Хризолидокарпус Лутесценс d-9 см | растения |
| 4 | Циперус Зумула d-12 см h-25 см | растения |
# добавим в основной датасет столбец с категорией товара, притянув её по имени товара
df = pd.merge(df, df_cat, on='product')
Проведём детальный анализ данных, чтобы определить тренды и сформулировать рекомендации для менеджеров магазина
# Построим столбчатую диаграмму распределения выручки и продаж по месяцам
dbm = df.groupby(['ymonth'])[['revenue','quantity']].sum() # cводная таблица с нужными цифрами
fig = plt.figure() # создаём график
ax = fig.add_subplot(111) # добавляем оси
ax2 = ax.twinx() # дублируем созданную ось
width = 0.3
# строим два графика
dbm.revenue.plot(kind='bar', color='magenta', ax=ax, width=width, position=1, label='Выручка')
dbm.quantity.plot(kind='bar', color='cyan', ax=ax2, width=width, position=0, label='Продажи')
# добавляем названия
plt.legend(loc="upper right")
ax.set_ylabel('Выручка в у.е.')
ax2.set_ylabel('Продажи в шт')
plt.title('Распределение продаж по месяцам в у.е. и шт')
plt.show()
Итак, мы анализируем оставшиеся продажи на 2,8 млн у.е., очищенные от оптовых и аномальных операций.
По распределению продаж видно, что пиковые продажи наблюдались осенью 2018 и постепенно снижались: к концу анализируемого периода продажи упали в полтора раза, а в октябре 2019 года продажи были почти вдвое меньше октября 2018-го.
При этом в январе и июне наблюдались спады продаж. Январский спад возможен по причине длительных праздников, а июньский, например, сезоном отпусков.
Распределение объёмов закупок товара в основном повторяет распределение выручки, за исключением двух моментов:
Посмотрим, как распределена выручка по покупателям и заказам.
# Построим столбчатую диаграмму распределения выручки и продаж по топ-10 покупателей
dbc = df.groupby(['cid'])[['revenue','quantity']].sum().sort_values('quantity', ascending=False).head(10)
fig = plt.figure() # создаём график
ax = fig.add_subplot(111) # добавляем оси
ax2 = ax.twinx() # дублируем созданную ось
width = 0.3
# строим два графика
dbc.revenue.plot(kind='bar', color='magenta', ax=ax, width=width, position=1, label='Выручка')
dbc.quantity.plot(kind='bar', color='cyan', ax=ax2, width=width, position=0, label='Продажи')
# добавляем названия
plt.legend(loc="upper right")
ax.set_ylabel('Выручка в у.е.')
ax2.set_ylabel('Продажи в шт')
plt.title('Распределение продаж по топ-10 покупателям в у.е. и шт')
plt.show()
Видим, что на двух самых крупных покупателей приходится 7% всех продаж, но уже на следующих менее 2%. Они же крупнейшие покупатели по количеству товаров.
Посмотрим, как часто они совершали покупки
# Построим диаграммы распределения выручки по месяцам по каждому из этих покупателей
display(df.query('cid in ["ee86d3b","e40d7db"]')
.pivot_table(index='ymonth', values='quantity', aggfunc='sum')
.reset_index()
.sort_values('ymonth', ascending=True)
.plot(x='ymonth', y='quantity', kind='bar')); # строим диаграмму
# называем оси и заголовок
plt.xlabel('Месяц')
plt.ylabel('Товары в шт')
plt.title('Закупки покупателей ee86d3b и e40d7db')
plt.show()
<AxesSubplot:xlabel='ymonth'>
Итак, они совершали покупки вплоть до марта 2019 года, но не далее. Вероятно, это упущенные клиенты
А как распределены товары по заказам?
# Построим столбчатую диаграмму распределения товаров по заказам
display(df.pivot_table(index='order_id', values='quantity', aggfunc='sum')
.reset_index()
.sort_values('quantity', ascending=False)
.head(10)
.plot(x='order_id', y='quantity', kind='bar')
) # cводная таблица с нужными цифрами, отсортированная по убыванию кол-ва точек
# называем оси и заголовок
plt.xlabel('Заказ')
plt.ylabel('Количество товара, шт')
plt.title('Распределение товаров по топ-10 заказов')
plt.show()
<AxesSubplot:xlabel='order_id'>
Поскольку мы исключили оптовые заказа, колебание кол-ва товаров в оставшихся заказов несильное.
Перейдём к анализу популярных товаров
# Построим столбчатую диаграмму продаж топ-10 популярных товаров
display(df.pivot_table(index='prod', values='quantity', aggfunc='sum')
.reset_index()
.sort_values('quantity', ascending=False)
.head(10)
.plot(x='prod', y='quantity', kind='bar')
) # cводная таблица с нужными цифрами, отсортированная по убыванию кол-ва точек
# называем оси и заголовок
plt.xlabel('Название товара (укор.)')
plt.ylabel('Количество товара, шт')
plt.title('Топ-10 популярных товаров')
plt.show()
<AxesSubplot:xlabel='prod'>
Из графика выше можно заключить, что больше всего магазин продаёт растения и цветы, в том числе искусственные.
Посмотрим, какая категория товаров пользуется наибольшим спросом.
Для этого мы присвоили каждому из товаров одну из шести следующих категорий: одежда и обувь, растения, дом и быт, кухонные принадлежности, хранение и перевозка, прочее.
# Построим диаграммы распределения выручки и количества по категориям товаров
plotpie = (df.pivot_table(index='cat', values=['revenue','quantity'], aggfunc='sum')
.reset_index()
.sort_values('revenue', ascending=False)
) # cводная таблица с нужными цифрами, отсортированная по убыванию кол-ва точек
# размещаем две диаграммы рядом друг с другом
fig, (ax1,ax2) = plt.subplots(1,2,figsize=(12,12))
# данные и описания для первой
labels = plotpie['cat']
values = plotpie['revenue']
ax1.pie(values,labels = labels,autopct = '%1.1f%%') #plot first pie
ax1.set_title('Выручка по категориям')
# данные и описания для второй
labels = plotpie['cat']
values = plotpie['quantity']
ax2.pie(values,labels = labels,autopct = '%1.1f%%') #plot second pie
ax2.set_title('Количество по категориям')
# выводим их на экран
plt.show();
Видно, что выручка и количество товара распределены неравномерно.
Во-первых, 44% продаж приходится на растения, но приносят они лишь 16% выручки. Вероятно, растения для посадки закупаются не круглый год, поэтому важно заполнять ими складские помещения именно накануне пикового спроса.
Во-вторых, самая малочисленная категория "хранение и перевозка" приносит более 21% выручки. За ней следуют, одежда с обувью и хозтовары, каждая из которых отвечает за пятую часть выручки.
Наконец, кухонные и прочие товары приносят лишь 18% выручки, занимая 25% продаж. Возможно, магазину стоит отказаться от закупок товаров этих категорий в пользу более прибыльных.
Посмотрим, как распределены продажи по времени: сперва по месяцам, а далее по дням недели и часам
catsum = df.groupby(['ymonth','cat'])[['quantity']].sum()
# переименуем столбец
catsum.columns = ['total_quantity']
# строим столбчатую диаграмму
fig = px.bar(catsum.reset_index().sort_values(by=['total_quantity'], ascending=False), # загружаем данные и заново их сортируем
x='total_quantity', # указываем столбец с данными для оси X
y='ymonth', # указываем столбец с данными для оси Y
color='cat',
)
# оформляем график
fig.update_layout(title='Продажи товаров по категориям',
xaxis_title='Количество в шт',
yaxis_title='Месяц',
yaxis={'categoryorder':'total ascending'})
fig.show() # выводим график
В поисках сезонности
Мы подтвердили нашу гипотезу, что продажи растений определяются сезонностью. Так, пик продаж приходится на весну, или апрель-май, когда начинается сезон посадок. Этот же факт отвечает на наше раннее наблюдение, что в апреле-мае наблюдаются пики продаж. Эта информация пригодится магазину для эффективного управления складскими помещениями.
Продажи одежды и обуви, наоборот, логично растут в в холодные осенние и зимние месяцы, падая к лету. То же самое происходит с кухонными и прочими товарами.
В продажах хозтоваров и ёмкостей не наблюдается ярко выраженной сезонности, однако они сокращаются к концу года. Сложно делать однозначный вывод о долгосрочном тренде на основании данных за октябрь 2018 и октябрь 2019 года, но по всех категориям товаров, кроме растений и ёмкостей, продажи падают к октябрю 2019 года в 1,5-4 раза. Это может свидетельствовать о низкой конкурентоспособности магазина во всех категориях, кроме растений.
Теперь построим хитмепы для анализа продаж в разрезе дней недели и часов
# строим пивот, от которого будем отрезать данные по категориям
heatmap_catd = (df.pivot_table(index=['cat','dow','hour'], values='quantity', aggfunc='sum')
.reset_index()
.sort_values('dow', ascending=True)
)
# приводим в табличный вид
plants_hm = (heatmap_catd.query('cat == "растения"')
.pivot('hour', 'dow', 'quantity')
)
# строим хитмеп нужного размера
fig, ax = plt.subplots(figsize=(5,5))
plt.title('Продажа растений в разрезе дней недели и часов')
display(sns.heatmap(plants_hm, annot=True));
<AxesSubplot:title={'center':'Продажа растений в разрезе дней недели и часов'}, xlabel='dow', ylabel='hour'>
# приводим в табличный вид
clothes_hm = (heatmap_catd.query('cat == "одежда и обувь"')
.pivot('hour', 'dow', 'quantity')
)
# строим хитмеп нужного размера
fig, ax = plt.subplots(figsize=(5,5))
plt.title('Продажа одежды и обуви в разрезе дней недели и часов')
display(sns.heatmap(clothes_hm, annot=True))
<AxesSubplot:title={'center':'Продажа одежды и обуви в разрезе дней недели и часов'}, xlabel='dow', ylabel='hour'>
# приводим в табличный вид
appliances_hm = (heatmap_catd.query('cat == "дом и быт"')
.pivot('hour', 'dow', 'quantity')
)
# строим хитмеп нужного размера
fig, ax = plt.subplots(figsize=(5,5))
plt.title('Продажа товаров для дома в разрезе дней недели и часов')
display(sns.heatmap(appliances_hm, annot=True))
<AxesSubplot:title={'center':'Продажа товаров для дома в разрезе дней недели и часов'}, xlabel='dow', ylabel='hour'>
# приводим в табличный вид
storage_hm = (heatmap_catd.query('cat == "хранение и перевозка"')
.pivot('hour', 'dow', 'quantity')
)
# строим хитмеп нужного размера
fig, ax = plt.subplots(figsize=(5,5))
plt.title('Продажа ёмкостей в разрезе дней недели и часов')
display(sns.heatmap(storage_hm, annot=True))
<AxesSubplot:title={'center':'Продажа ёмкостей в разрезе дней недели и часов'}, xlabel='dow', ylabel='hour'>
Во всех случаях продажи практически отсутствуют в ночное время до 9 часов утра. Пик приходится на 9-15 часов, а растения активно покупают и после 15.
Кроме продаж ёмкостей, которые распределены более-менее равномерно по дням, основной объём продаж приходится на понедельник-среду.
Поскольку в дневные часы наблюдается основной поток заказов, можно было бы ввести скидки на заказы, совершаемые поздно вечером, чтобы сгладить нагрузку на операторов.
Посмотрим, можем ли мы отделить категории товаров, которые покупают вместе с другими товарами, от покупаемых отдельно.
Основной и дополнительный ассортимент
# сделаем пивот по номерам заказов и количеством уникальных товаров в каждом
orders_ops = (df.pivot_table(index='order_id', values='prod', aggfunc='nunique')
.reset_index()
.sort_values('prod', ascending=False)
)
# приведём кол-во к целочисленному типу данных
orders_ops['prod'] = orders_ops['prod'].astype('int')
# оставим только заказы, в которых покупают один уникальный товар
orders_ops = orders_ops['order_id'].loc[orders_ops['prod'] == 1]
# переведём серию в список
orders_ops = orders_ops.tolist()
orders_ops
[71510, 71538, 71516, 71934, 71513, 71530, 71529, 71936, 71945, 71938, 71523, 71939, 71519, 71937, 71522, 71526, 71515, 71525, 71524, 71514, 71941, 71552, 71541, 71565, 71560, 71915, 71911, 71909, 71908, 71561, 71566, 71922, 71568, 71572, 71573, 71907, 71574, 71578, 71918, 71559, 71933, 71551, 71931, 71546, 71930, 71547, 71549, 71550, 71505, 71924, 71555, 71556, 71557, 71558, 71928, 71926, 71509, 71948, 71504, 71391, 71392, 71393, 71396, 71967, 71397, 71398, 71966, 71965, 71963, 71961, 71399, 71400, 71409, 71411, 71412, 71972, 71974, 71502, 71390, 71352, 71353, 71354, 71362, 71363, 71364, 71365, 71976, 71368, 71370, 71375, 71378, 71385, 71388, 71389, 71413, 71414, 71415, 71959, 71458, 71460, 71462, 71463, 71950, 71466, 71581, 71467, 71469, 71470, 71479, 71484, 71491, 71498, 71499, 71457, 71952, 71456, 71426, 71957, 71417, 71956, 71419, 71422, 71955, 71954, 71454, 71428, 71953, 71432, 71440, 71441, 71450, 71580, 71792, 71903, 71725, 71713, 71716, 71717, 71723, 71724, 71850, 71849, 71848, 71860, 71846, 71844, 71728, 71837, 71732, 71835, 71834, 71708, 71705, 71703, 71701, 71855, 71685, 71687, 71689, 71690, 71691, 71693, 71694, 71695, 71696, 71853, 71852, 71697, 71698, 71700, 71733, 71349, 71833, 71805, 71803, 71761, 71764, 71802, 71765, 71767, 71801, 71770, 71780, 71785, 71786, 71800, 71788, 71799, 71789, 71760, 71807, 71831, 71808, 71736, 71738, 71829, 71828, 71744, 71750, 71751, 71753, 71755, 71756, 71758, 71817, 71816, 71759, 71810, 71858, 71682, 71902, 71617, 71604, 71605, 71606, 71612, 71614, 71615, 71894, 71620, 71681, 71621, 71623, 71630, 71633, 71634, 71893, 71635, 71603, 71895, 71898, 71601, 71587, 71901, 71589, 71900, 71591, 71592, 71798, 71593, 71594, 71595, 71899, 71597, 71598, 71599, 71600, 71636, 71892, 71638, 71662, 71664, 71876, 71665, 71874, 71667, 71670, 71865, 71673, 71674, 71863, 71677, 71678, 71862, 71679, 71680, 71877, 71878, 71889, 71661, 71639, 71886, 71640, 71642, 71643, 71644, 71645, 71646, 71647, 71885, 71657, 71882, 71881, 71880, 71879, 71351, 12624, 71347, 70861, 70905, 70908, 70909, 70913, 70915, 70916, 70919, 70921, 70922, 70924, 70925, 70931, 70932, 70933, 70934, 70938, 70939, 70904, 70899, 70897, 70876, 70863, 70864, 70866, 70869, 70870, 70872, 70874, 70877, 70896, 70878, 70881, 70883, 70885, 70888, 70891, 70895, 70943, 70947, 70949, 71012, 70999, 71000, 71004, 71007, 71008, 71009, 71010, 71014, 70988, 71015, 71016, 71017, 71018, 71023, 71027, 71028, 70992, 70986, 70951, 70965, 70953, 70956, 70959, 70960, 70961, 70962, 70964, 70968, 70983, 70973, 70974, 70976, 70978, 70980, 70981, 70982, 70862, 70860, 71030, 70859, 70742, 70743, 70746, 70747, 70748, 70749, 70751, 70755, 70757, 70758, 70762, 70764, 70765, 70766, 70769, 70770, 70772, 70741, 70740, 70736, 70710, 70693, 70694, 70697, 70702, 70704, 70708, 70709, 70712, 70734, 70713, 70718, 70721, 70723, 70725, 70727, 70728, 70773, 70774, 70776, 70843, 70832, 70833, 70834, 70837, 70839, 70840, 70842, 70845, 70828, 70846, 70847, 70851, 70853, 70854, 70856, 70857, 70831, 70822, 70782, 70800, 70785, 70788, 70793, 70794, 70796, 70797, 70799, 70801, 70821, 70803, 70807, 70809, 70812, 70814, 70818, 70819, 71029, 71031, 71345, 71192, 71230, 71232, 71233, 71234, 71239, 71240, 71242, 71244, 71245, 71247, 71248, 71249, 71251, 71253, 71254, 71255, 71256, 71227, 71225, 71223, 71207, 71195, 71197, 71198, 71200, 71202, 71205, 71206, 71209, 71222, 71211, 71213, 71214, 71217, 71218, 71219, 71220, 71257, 71258, 71261, 71328, 71309, 71310, 71311, 71319, 71320, 71322, 71324, 71329, 71306, 71330, 71331, 71333, 71335, 71336, 71341, 71344, 71307, 71302, 71262, 71279, 71264, 71265, 71266, 71267, 71271, 71272, 71275, 71284, 71301, 71287, 71288, 71289, 71291, 71294, 71299, 71300, 71193, 71191, 71032, 71188, 71074, 71075, 71076, 71077, 71080, 71083, 71085, 71088, 71089, 71091, 71093, 71094, 71098, 71101, 71103, 71107, 71108, 71071, 71065, 71063, 71043, 71033, 71034, 71035, 71038, 71039, 71041, 71042, 71044, 71057, 71045, 71046, 71048, 71049, 71050, 71053, 71055, 71111, 71113, 71121, 71170, 71156, 71157, 71159, 71160, 71162, 71163, 71164, 71171, 71154, 71174, 71980, 71175, 71176, 71177, 71178, 71186, 71155, 71153, 71122, 71133, 71124, 71125, 71126, 71127, 71128, 71130, 71131, 71136, 71152, 71138, 71139, 71140, 71141, 71142, 71148, 71149, 71979, 72220, 71984, 72788, 72773, 72776, 72779, 72780, 72781, 72786, 72787, 72791, 72727, 72792, 72793, 72794, 72795, 72796, 72797, 72799, 72772, 72771, 72770, 72769, 72732, 72734, 72741, 72742, 72744, 72745, 72746, 72747, 72748, 72753, 72759, 72764, 72765, 72766, 72767, 72800, 72801, 72802, 72836, 72844, 72847, 72848, 72849, 72852, 72854, 72858, 72859, 72861, 72862, 72863, 72865, 72866, 72867, 72868, 72843, 72834, 72803, 72833, 72805, 72806, 72810, 72814, 72817, 72818, 72819, 72820, 72821, 72824, 72826, 72827, 72828, 72829, 72831, 72729, 72726, 72870, 72617, 72608, 72609, 72611, 72613, 72614, 72615, 72616, 72618, 72722, 72621, 72625, 72627, 72628, 72634, 72635, 72637, 72606, 72605, 72600, 72598, 72568, 72571, 72574, 72577, 72578, 72581, 72582, 72583, 72585, 72586, 72587, 72590, 72592, 72593, 72595, 72638, 72639, 72641, 72686, 72690, 72691, 72695, 72696, 72697, 72704, 72707, 72710, 72713, 72714, 72715, 72717, 72718, 72719, 72720, 72689, 72684, 72648, 72683, 72649, 72651, 72652, 72653, 72657, 72658, 72660, 72667, 72673, 72674, 72675, 72678, 72679, 72681, 72682, 72869, 72871, 72566, 73060, 73046, 73047, 73050, 73051, 73052, 73055, 73057, 73063, 73000, 73066, 73068, 73069, 73071, 73072, 73073, 73074, 73044, 73041, 73040, 73039, 73003, 73008, 73014, 73015, 73016, 73017, 73025, 73027, 73030, 73031, 73032, 73034, 73036, 73037, 73038, 73077, 73082, 73083, 73136, 73138, 73140, 73141, 73142, 73143, 73144, 73146, 73147, 73148, 73151, 73154, 73155, 73156, 73158, 73162, 73137, 73131, 73084, 73130, 73086, 73092, 73093, 73094, 73095, 73097, 73101, 73104, 73105, 73108, 73112, 73115, 73123, 73126, 73129, 73002, 72999, 72872, 72922, 72912, 72913, 72914, 72915, 72917, 72918, 72919, 72923, 72998, 72924, 72925, 72926, 72927, 72929, 72930, 72931, 72909, 72907, 72905, 72904, 72873, 72874, 72876, 72881, 72883, 72884, 72889, 72890, 72892, 72893, 72896, 72899, 72900, 72901, 72903, 72934, 72936, 72937, 72974, 72976, 72977, 72978, 72981, 72983, 72984, 72986, 72987, 72988, 72991, 72992, 72993, 72995, 72996, 72997, 72975, 72973, 72938, 72969, 72940, 72942, 72944, 72945, 72946, 72947, 72949, 72951, 72952, 72953, 72954, 72958, 72959, 72965, 72967, 72567, 72564, 71985, 72170, 72159, 72161, 72162, 72164, 72165, 72168, 72169, 72173, 72123, 72177, 72179, 72180, 72183, 72189, 72190, 72193, 72153, 72152, 72151, 72150, 72125, 72126, 72127, 72128, 72129, 72130, 72132, 72133, 72134, 72135, 72139, 72140, 72141, 72142, 72149, 72194, 72195, 72196, 70690, 72225, 72227, 72228, 72229, 72230, 72231, 72232, 72233, 72235, 72237, 72240, 72249, 72250, 72252, 72256, 72223, 72219, 72197, 72218, 72198, 72200, 72201, 72202, 72204, 72205, 72206, 72208, 72209, 72210, 72211, 72212, 72214, 72216, 72217, 72124, 72119, 72262, 72035, 72022, 72026, ...]
# добавим столбец в датасет, отвечающий на вопрос, один ли уникальный товар содержится в заказе или нет
df['sole_order'] = [x in orders_ops for x in df['order_id']]
df['sole_order']
0 False
1 False
2 False
3 False
4 False
...
4778 True
4779 True
4780 True
4781 True
4782 True
Name: sole_order, Length: 4783, dtype: bool
# Построим диаграммы распределения продажи товаров по категориям (заказы с одним уникальным товаром)
display(df.query('sole_order == True')
.pivot_table(index='cat', values='quantity', aggfunc='sum')
.reset_index()
.sort_values('quantity', ascending=False)
.plot(x='cat', y='quantity', kind='bar')); # строим диаграмму
# называем оси и заголовок
plt.xlabel('Категория')
plt.ylabel('Товары в шт')
plt.title('Если в заказе один товар, то какой категории?')
plt.show()
<AxesSubplot:xlabel='cat'>
# Построим диаграммы распределения продажи товаров по категориям (заказы с несколькими уникальными товарами)
display(df.query('sole_order == False')
.pivot_table(index='cat', values='quantity', aggfunc='sum')
.reset_index()
.sort_values('quantity', ascending=False)
.head(15)
.plot(x='cat', y='quantity', kind='bar')); # строим диаграмму
# называем оси и заголовок
plt.xlabel('Категория')
plt.ylabel('Товары в шт')
plt.title('Если в заказе несколько товаров, то каких категорий?')
plt.show()
<AxesSubplot:xlabel='cat'>
Итак, мы имеем следующее соотношение основного и дополнительного ассортимента по каждой категории:
1. Растения: 52% основного и 48% дополнительного
2. Дом и быт: 84% и 16%
3. Прочее: 82% и 18%
4. Кухня: 82% и 18%
5. Одежда и обувь: 85% и 15%
6. Хранение и перевозка: 91% и 9%
Посмотрим, какие товары входят в основной и допассортимент.
# посмотрим и сравним топ-3 товара каждой категории основного и дополнительного ассортимента
for i in df['cat'].unique(): # перебираем все категории
cats = (df.query('cat == @i')
.groupby(['sole_order','cat','prod'])[['quantity']].sum()
.sort_values(by=['cat','quantity'], ascending=False)
.reset_index()
)
for k in df['sole_order'].unique(): # перебираем основной и допассортимент
display(cats.query('sole_order == @k').head(3)) # выводим топ-3 товара по количеству продаж
| sole_order | cat | prod | quantity | |
|---|---|---|---|---|
| 3 | False | растения | Пеларгония зональная | 239 |
| 5 | False | растения | Пеларгония розебудная | 156 |
| 6 | False | растения | Рассада зелени | 149 |
| sole_order | cat | prod | quantity | |
|---|---|---|---|---|
| 0 | True | растения | Искусственный цветок | 323 |
| 1 | True | растения | Цветок искусственный | 298 |
| 2 | True | растения | Пеларгония зональная | 249 |
| sole_order | cat | prod | quantity | |
|---|---|---|---|---|
| 10 | False | одежда и обувь | Вешалка деревянная | 20 |
| 11 | False | одежда и обувь | Вешалка для | 19 |
| 12 | False | одежда и обувь | Плечики пластмассовые | 19 |
| sole_order | cat | prod | quantity | |
|---|---|---|---|---|
| 0 | True | одежда и обувь | Сушилка для | 278 |
| 1 | True | одежда и обувь | Гладильная доска | 125 |
| 2 | True | одежда и обувь | Вешалка для | 81 |
| sole_order | cat | prod | quantity | |
|---|---|---|---|---|
| 5 | False | дом и быт | Щетка для | 52 |
| 7 | False | дом и быт | Щетка-утюжок с | 50 |
| 15 | False | дом и быт | Набор вешалок | 28 |
| sole_order | cat | prod | quantity | |
|---|---|---|---|---|
| 0 | True | дом и быт | Ёрш унитазный | 103 |
| 1 | True | дом и быт | Коврик придверный | 98 |
| 2 | True | дом и быт | Щетка-сметка 4-х | 90 |
| sole_order | cat | prod | quantity | |
|---|---|---|---|---|
| 10 | False | хранение и перевозка | Банка стеклянная | 7 |
| 15 | False | хранение и перевозка | Короб стеллажный | 4 |
| 17 | False | хранение и перевозка | Ящик для | 3 |
| sole_order | cat | prod | quantity | |
|---|---|---|---|---|
| 0 | True | хранение и перевозка | Сумка-тележка хозяйственная | 111 |
| 1 | True | хранение и перевозка | Сумка-тележка 2-х | 92 |
| 2 | True | хранение и перевозка | Тележка багажная | 65 |
| sole_order | cat | prod | quantity | |
|---|---|---|---|---|
| 5 | False | кухонные принадлежности | Нож кухонный | 33 |
| 6 | False | кухонные принадлежности | Кружка НОРДИК | 30 |
| 12 | False | кухонные принадлежности | Тарелка суповая | 13 |
| sole_order | cat | prod | quantity | |
|---|---|---|---|---|
| 0 | True | кухонные принадлежности | Таз пластмассовый | 121 |
| 1 | True | кухонные принадлежности | Тарелка обеденная | 119 |
| 2 | True | кухонные принадлежности | Тарелка десертная | 80 |
| sole_order | cat | prod | quantity | |
|---|---|---|---|---|
| 8 | False | прочее | Муляж Апельсин | 29 |
| 10 | False | прочее | Муляж Яблоко | 25 |
| 14 | False | прочее | Муляж Красное | 20 |
| sole_order | cat | prod | quantity | |
|---|---|---|---|---|
| 0 | True | прочее | Муляж Яблоко | 124 |
| 1 | True | прочее | Муляж Банан | 105 |
| 2 | True | прочее | Муляж Лимон | 91 |
Различия в популярных товарах разных ассортиметов следующие:
Таким образом, зачастую ассортименты пересекаются, но по количеству можно выделить следующих лидеров основного ассортимента:
# Построим диаграммы частоты категории товаров в заказах с несколькими уникальными товарами
display(df.query('sole_order == False')
.pivot_table(index='order_id', values='cat', aggfunc='nunique')
.reset_index()
.sort_values('cat', ascending=False)
.hist('cat',bins=6)
); # строим диаграмму
# называем оси и заголовок
plt.xlabel('Категория')
plt.ylabel('Заказы в шт')
plt.title('В 200 заказах встречаются товары только одной категории')
plt.show()
array([[<AxesSubplot:title={'center':'cat'}>]], dtype=object)
# Построим диаграммы частоты категории товаров в заказах с несколькими уникальными товарами
display(df.query('sole_order == False')
.pivot_table(index='cat', values='order_id', aggfunc='nunique')
.reset_index()
.sort_values('order_id', ascending=False)
.plot(x='cat', y='order_id',kind='bar')
); # строим диаграмму
# называем оси и заголовок
plt.xlabel('Категория')
plt.ylabel('Товары в шт')
plt.title('Из 277 таких заказов растения есть более чем в 200')
plt.show()
# сколько всего таких заказов?
display(df.query('sole_order == False')['order_id'].nunique())
<AxesSubplot:xlabel='cat'>
277
Итак, мы видим, что товары всех категорий, исключая растения, в 5 случаях из 6 (или чаще) покупаются отдельно. Растения покупают отдельно в 40% процентах случаев.
Однако могут быть ситуации, когда в заказе есть несколько уникальных товаров, но все они принадлежат одной категории. Мы выяснили, что таких заказов более двухсот. При этом из 277 заказов, в которых присутствует 2 и более уникальных товара, в 200 есть растения.
Таким образом, хотя 60% растений покупаются вместе с другими товарами, эти другие товары в основном тоже растения.
Критерий вывода о значимости/незначимости различий: Непараметрический тест Уилкоксона-Манна-Уитни
Уровень значимости: 0,05
Нулевая гипотеза: В средней выручке между товарами основного и допассортимента по очищенным данным отсутствуют статистически значимые различия
Альтернативная гипотеза: Различия в средней выручке между товарами основного и допассортимента по очищенным данным статистически значимы
print('p-value:', "{0:.3f}".format(stats.mannwhitneyu(df[df['sole_order']== True]['revenue'],
df[df['sole_order']== False]['revenue'])[1]))
print('относительное различие средней выручки','{0:.3f}'.format(df[df['sole_order']== True]['revenue'].mean()/df[df['sole_order']== False]['revenue'].mean()-1))
p-value: 0.000 относительное различие средней выручки 3.764
Критерий вывода о значимости/незначимости различий: Непараметрический тест Уилкоксона-Манна-Уитни
Уровень значимости: 0,05
Нулевая гипотеза: В среднем количестве между товарами основного и допассортимента по очищенным данным отсутствуют статистически значимые различия
Альтернативная гипотеза: Различия в среднем количестве между товарами основного и допассортимента по очищенным данным статистически значимы
print('p-value:', "{0:.3f}".format(stats.mannwhitneyu(df[df['sole_order']== True]['quantity'],
df[df['sole_order']== False]['quantity'])[1]))
print('относительное различие среднего количества','{0:.3f}'.format(df[df['sole_order']== True]['quantity'].mean()/df[df['sole_order']== False]['quantity'].mean()-1))
p-value: 0.000 относительное различие среднего количества 0.597
В обоих случаях p-value меньше уровня значимости, поэтому принимаем гипотезы об отсутствии различий как в средней выручке, так и в среднем количестве проданных товаров основного и допассортимента.
Проанализировав полученные данные, мы пришли к следующим выводам:
1. Данные по продажам охватывают 1 год и 1 месяц: с октября 2018 по октябрь 2019 года.
2. Мы обнаружили пропуски в нумерации заказов - вопрос к техподдержке о её адекватности.
3. Это розничный магазин, торгующий товарами преимущественно до 1 000 у.е.
4. Полных дубликатов не обнаружено, но есть как дублирующиеся в разное время заказы, так и ошибочные пары покупатель-заказ. Они заняли около 30% датасета.
5. Оптовые продажи составили 4,5% операций и треть (1,3 млн у.е.) выручки, в том числе единичная закупка 1000 единиц товара (вероятно, ошибочная запись).
6. Розничные продажи составили 2,8 млн у.е. Помесячный тренд говорит о постепенном сокращении выручки как в монетарном, так и в физическом выражении. Наиболее крупные по объёму закупки наблюдаются в апреле и мае, в пик продаж растений.
7. Структура продаж неоднородная:
Во-первых, более половины продаж приходится на растения, но приночят они лишь 17% выручки. Вероятно, растения для посадки закупаются не круглый год, поэтому важно заполнять ими складские помещения именно накануне пикового спроса.
Во-вторых, самая малочисленная категория "хранение и перевозка" приносит больше всего (четверть) выручки. За ней следуют, одежда с обувью и хозтовары, каждая из которых отвечает за пятую часть выручки.
Наконец, кухонные и прочие товары приносят лишь 15% выручки, занимая 17% продаж. Возможно, магазину стоит отказаться от закупок товаров этих категорий в пользу более прибыльных.
8. Сезонность наблюдается в продажах растений (апрель-май) и одежды и обуви (осень-зима). По дням недели распределение практически равномерное, основной объём продаж приходится на первую половину дня, или с 9 до 15 часов (до 18 для растений).
9. 85% товаров всех категорий, исключая растения, продаются отдельно. Растения продаются как отдельно (40%), так и вместе с другими товарами. Однако большинство этих других товаров - другие растения.
Рекомендации:
1. Подготовить для операторов заказов единую инструкцию по занесению данных в базу, чтобы не было пропусков и корявых заказов.
2. Возможно, стоит придумать выгодные предложения для оптовых покупателей: их немного, но треть выручки приходится на них.
3. Внести корректировки в систему закупок и управления складами. Растения занимают больше всего места, но продаются практически только весной. Соответственно, осенью-зимой можно было бы освободить больше места для других категорий товаров: одежды и обуви, нужные в холод, и ёмкостей, которые приносят больше выручки.
4. Следует сократить или отказаться от закупки кухонных и прочих товаров в пользу более прибыльных.
5. Подумать над системой скидок при заказе товаров вечером, чтобы разгрузить операторов.